跳到主要内容

Java 面试题积累

数值取值范围相关

因为这类问题太普遍了,所以这里记录一下

顺便补充一下取模操作,免得老是忘记...

5 % 2 = 1,
1 % 2 = 1

String 是否有拆箱?

String str = new String("hello");
System.out.println(str == "hello");

返回的是 false,因为 String 类型不是基本类型,所以不存在拆箱这一操作

finally 是否会在 return 后执行

下面程序的输出结果为( )

public class Demo {
public static String sRet = "";

public static void func(int i) {
try {
// 5 % 2 = 1,
// 1 % 2 = 1
if (i % 2 == 0) {
throw new Exception();
}
} catch (Exception e) {
sRet += "0";
return;
} finally {
sRet += "1";
}
sRet += "2";
}

public static void main(String[] args) {
func(1);
func(2);
System.out.println(sRet);
}
}

A、120 B、1201 C、12012 D、101

  1. 调用 func(1) ,if 不符合,直接进入 finally,sRet="1"
  2. finally 语句中没有返回值,故继续向下执行,sRet="12"
  3. 调用 func(2) ,if符合,sRet = "120",此时有返回值!!!
  4. 调用 finally 语句,sRet="1201"
  5. 因为已经有返回值了,finally 之后的语句也不再执行,sRet="1201"。

这题的关键就是 finally 是否会在 return 后执行?

答案是肯定的,在 try 执行完成之后,finally 是一定会执行的。这种特性可以让程序员避免在 try 语句中使用了 return,continue 或者 break 关键字而忽略了关闭相关资源的操作。

PS: 用到 finally 关闭资源的时候,应该尽量避免在 finally 语句块中出现运行时错误,可以适当添加判断语句以增加程序健壮性:

finally {
if (out != null) {
System.out.println("Closing PrintWriter");
out.close(); // 不要在finally语句中直接调用close()
} else {
System.out.println("PrintWriter not open");
}
}

finalize 方法的执行

finalize 是 Object 类的一个方法,在垃圾回收器执行时会调用被回收对象的 finalize() 方法,可以覆盖此方法来实现对其他资源的回收(一旦垃圾回收器准备好释放对象占用的空间,将首先调用该方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存),从功能上来说,finalize() 方法与 c++ 中的析构函数比较相似,但是 Java 采用的是基于垃圾回收器的自动内存管理机制,所以 finalize() 方法在本质上不同于 C++ 中的析构函数。

判定一个对象 objA 是否可回收,至少要经历两次标记过程:

1、如果对象 objA 到 GC Roots 没有引用链,则进行第一次标记。

进行筛选,判断此对象是否有必要执行 finalize() 方法

如果对象 objA 没有重写 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,则虚拟机视为 “没有必要执行”,objA 被判定为不可触及的。

如果对象 objA 重写了 finalize() 方法,且还未执行过,那么 objA 会被插入到 F-Queue 队列中,由一个虚拟机自动创建的、低优先级的 Finalizer 线程触发其 finalize() 方法执行。

finalize() 方法是对象逃脱死亡的最后机会,稍后 GC 会对 F-Queue 队列中的对象进行第二次标记。如果 objA 在 finalize() 方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,objA 会被移出 “即将回收” 集合。

之后,对象会再次出现没有引用存在的情况。在这个情况下,finalize 方法不会被再次调用,对象会直接变成不可触及的状态,也就是说,一个对象的 finalize 方法只会被调用一次。

构造方法在被哪些地方调用?

问题:用户不能调用构造方法,只能通过 new 关键字自动调用。(错误)

解析:

1、在类内部可以用户可以使用关键字 this.构造方法名() 调用(参数决定调用的是本类对应的构造方法)

2、在子类中用户可以通过关键字 super.父类构造方法名() 调用(参数决定调用的是父类对应的构造方法。)

3、反射机制对于任意一个类,都能够知道这个类的所有属性和方法,包括类的构造方法。

字符串的垃圾回收

static String str0 = "0123456789";
static String str1 = "0123456789";
String str2 = str1.substring(5);
String str3 = new String(str2);
String str4 = new String(str3.toCharArray());
str0 = null;

假定 str0,...,str4 后序代码都是只读引用。在 Java 7 中,以上述代码为基础,在发生过一次 FullGC 后,上述代码在 Heap 空间(不包括 PermGen)保留的字符数为()

A、5 B、10 C、15 D、20

解析:这是一个关于 Java 的垃圾回收机制的题目。垃圾回收主要针对的是堆区的回收,因为栈区的内存是随着线程而释放的。堆区分为三个区:

  • 年轻代(Young Generation)
  • 年老代(Old Generation)
  • 永久代(Permanent Generation,也就是方法区)。

年轻代:对象被创建时(new)的对象通常被放在 Young(除了一些占据内存比较大的对象),经过一定的 Minor GC(针对年轻代的内存回收)还活着的对象会被移动到年老代(一些具体的移动细节省略)。

年老代:就是上述年轻代移动过来的和一些比较大的对象。Major GC/Full GC 是针对年老代的回收

永久代:存储的是 final 常量,static 变量,常量池。

str3,str4 都是直接 new 的对象,而 substring 的源代码其实也是 new String 对象返回,而经过 fullgc 之后,年老区的内存回收,则年轻区的占了15个,不 PermGen。所以答案选 C

注意:str1 在常量池里,常量池是在 PermGen 中,不属于 Heap 空间,而这里是因为 str1.substring(5) 截取的字符串要重新放一个字符串到堆中

Java 构造代码块

public class HelloA {
public HelloA() {
System.out.println("A的构造函数");
}

{
System.out.println("A的构造代码块");
}

static {
System.out.println("A的静态代码块");
}

public static void main(String[] args) {
HelloA a = new HelloA();
}
}

关于构造代码块,以下几点要注意:

构造代码块的作用是给对象进行初始化。

1、对象一建立就运行构造代码块了,而且优先于构造函数执行。这里要强调一下,有对象建立,才会运行构造代码块,类不能调用构造代码块的,而且构造代码块与构造函数的执行顺序是前者先于后者执行。

2、构造代码块与构造函数的区别是:构造代码块是给所有对象进行统一初始化,而构造函数是给对应的对象初始化,因为构造函数是可以多个的,运行哪个构造函数就会建立什么样的对象,但无论建立哪个对象,都会先执行相同的构造代码块。也就是说,构造代码块中定义的是不同对象共性的初始化内容。

垃圾收集机制:新生代、老年代、持久代

现代垃圾收集器大部分都基于分代收集理论设计,堆空间细分为:

image.png

Java 7及之前堆内存逻辑上分为三部分:新生区 + 养老区 + 永久区

  • Young Generation Space 新生区 Young/New 又被划分为 Eden区和 Survivor区
  • Tenure Generation space 养老区 Old/Tenure
  • Permanent Space 永久区 Perm

Java 8及之后堆内存逻辑上分为三部分:新生区 + 养老区 + 元空间

  • Young Generation Space新生区 Young/New 又被划分为 Eden区和 Survivor区
  • Tenure Generation space 养老区 Old/Tenure
  • Meta Space 元空间 Meta

约定:下面几个名词代表的意思是一样的

  • 新生区 = 新生代 = 年轻代
  • 养老区 = 老年区 = 老年代
  • 永久区 = 永久代

新生代:

  1. 所有对象创建在新生代的 Eden 区,当 Eden 区满后触发新生代的 Minor GC,将 Eden 区和非空闲 Survivor 区存活的对象复制到另外一个空闲的 Survivor 区中。
  2. 保证一个 Survivor 区是空的,新生代 Minor GC 就是在两个 Survivor 区之间相互复制存活对象,直到 Survivor 区满为止。

老年代:当 Survivor 区也满了之后就通过 Minor GC 将对象复制到老年代。老年代也满了的话,就将触发 Full GC,针对整个堆(包括新生代、老年代、持久代)进行垃圾回收。

持久代:持久代如果满了,将触发 Full GC。

基类构造方法的执行

下面代码的输出是什么?

public class Base {
private String baseName = "base";
public Base() {
callName();
}

public void callName() {
System. out. println(baseName);
}

static class Sub extends Base {
private String baseName = "sub";
public void callName() {
System. out. println (baseName) ;
}
}

public static void main(String[] args) {
Base b = new Sub();
}
}

这里的 new Sub(); 在创造派生类的过程中首先创建基类对象,然后才能创建派生类。

创建基类即默认调用 Base() 方法,在方法中调用 callName() 方法,由于派生类中存在此方法,则被调用的 callName() 方法是派生类中的方法,此时派生类还未构造,所以变量 baseName 的值为 null

因此这里打印的是 null

substring 的取值范围

下面这个 b 的字符串是什么?

String a = "Hello";
String b = a.substring(0 , 2);

substring 方法后面跟的两个 int 值的索引下标是一个左闭右开的集合,即返回一个包含从 start 到最后(不包含 end)的子字符串的字符串。

所以这里 b 的字符串是 "He"

forward 和 redirect

下面有关 forward(转发)和 redirect(重定向)的描述,正确的是 () ?

A、forward 是服务器将控制权转交给另外一个内部服务器对象,由新的对象来全权负责响应用户的请求 B、执行 forward 时,浏览器不知道服务器发送的内容是从何处来,浏览器地址栏中还是原来的地址 C、执行 redirect 时,服务器端告诉浏览器重新去请求地址 D、forward 是内部重定向,redirect 是外部重定向 E、redirect 默认将产生 301 Permanently moved 的 HTTP 响应

答案是 B C D

从地址栏显示来说:

  • forward 是服务器请求资源,服务器直接访问目标地址的 URL,把那个 URL 的响应内容读取过来,然后把这些内容再发给浏览器。浏览器根本不知道服务器发送的内容从哪里来的,所以它的地址栏还是原来的地址。

  • redirect 是服务端根据逻辑,发送一个状态码,告诉浏览器重新去请求那个地址。所以地址栏显示的是新的 URL。

从数据共享来说:

  • forward:转发页面和转发到的页面可以共享 request 里面的数据。
  • redirect:不能共享数据。

从使用场景来说:

  • forward:一般用于用户登陆的时候,根据角色转发到相应的模块。
  • redirect:一般用于用户注销登陆时返回主页面和跳转到其它的网站等。

从效率来说:

  • forward:高
  • redirect:低

Class 文件规范相关问题

  1. JAVA 程序的 main 方法必须写在类里面
  2. JAVA 程序中 public 修饰的类名必须与文件名一样
  3. JAVA 程序的 main 方法中,不管有多少条语句都必须用 {}(大括号)括起来
  4. Java 中基本的编程单元为类(不是函数)

集合的继承关系